// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Author: Markus Gutschke

#include <math.h>
#include <stdlib.h>
#include <string.h>

#include "check.h"
#include "json.h"
#include "utf8.h"


static const char *parseCh(const char *txt, unsigned *codepoint) {
  // Parses and unescapes the contents of a string. Strings are stored in
  // UTF-8, and use JavaScript-style escape sequences to represent UTF-16
  // code points.
  txt = parseUTF8(txt, codepoint);
  if (!txt || *codepoint == '"') {
    return NULL;
  } else if (*codepoint == '\\') {
    unsigned ch;
    txt = parseUTF8(txt, &ch);
    check(txt);
    switch (ch) {
    case '"': case '\\': case '/':
      *codepoint = ch;
      return txt;
    case 'b':
      *codepoint = '\b';
      return txt;
    case 'f':
      *codepoint = '\f';
      return txt;
    case 'n':
      *codepoint = '\n';
      return txt;
    case 'r':
      *codepoint = '\r';
      return txt;
    case 't':
      *codepoint = '\t';
      return txt;
    case 'u': {
      unsigned cp = 0;
      for (int i = 0; i < 4; ++i) {
        txt = parseUTF8(txt, &ch);
        check(txt && ch >= '0' && (ch <= '9' || ch >= 'A') &&
              (ch <= 'F' || ch >= 'a') && ch <= 'f');
        cp = 16u*cp + (ch & 0xFu) + (ch > '9' ? 9u : 0u);
      }
      if (cp < 0xD800 || cp >= 0xE000) {
        *codepoint = cp;
        return txt;
      }
      check(cp >= 0xD800 && cp < 0xDC00);
      txt = parseUTF8(txt, &ch);
      check(txt && ch == '\\');
      txt = parseUTF8(txt, &ch);
      check(txt && ch == 'u');
      unsigned low = 0;
      for (int i = 0; i < 4; ++i) {
        txt = parseUTF8(txt, &ch);
        check(txt && ch >= '0' && (ch <= '9' || ch >= 'A') &&
              (ch <= 'F' || ch >= 'a') && ch <= 'f');
        low = 16u*low + (ch & 0xFu) + (ch > '9' ? 9u : 0u);
      }
      check(low >= 0xDC00 && low < 0xE000);
      *codepoint = ((cp - 0xD800u) << 10) + (low - 0xDC00u);
      return txt;
    }
    default:
      check(0);
    }
  } else {
    return txt;
  }
}

static char *parseString(const char **txt) {
  char *s = NULL;
  int sz = 0, len = 0;
  for (;;) {
    unsigned codepoint;
    const char *next = parseCh(*txt, &codepoint);
    check(next || codepoint == '"');
    if (sz < len + 6) {
      check(s = realloc(s, sz += 32));
    }
    if (next) {
      *txt = next;
      len = encodeUTF8(s + len, codepoint) - s;
    } else {
      s[len++] = '\000';
      check(s = realloc(s, len));
      ++*txt;
      break;
    }
  }
  return s;
}

static void destroyJSONEntry(void *arg, char *key, char *value) {
  (void)arg;
  free(key);
  deleteJSON((struct JSON *)value);
}

static const char *parseJSON(struct JSON *json, const char *txt) {
  for (; *txt > '\000' && *txt <= ' '; ++txt) { }
  check(*txt);
  switch (*txt) {
  case '\"': {
    check(*++txt);
    json->type = J_STRING;
    json->str  = parseString(&txt);
    break;
  }
  case '+': case '-': case '.': case '0': case '1': case '2': case '3':
  case '4': case '5': case '6': case '7': case '8': case '9': {
    const char *start = txt;
    if (*txt == '+' || *txt == '-') {
      ++txt;
    }
    check(*txt == '.' || (*txt >= '1' && *txt <= '9'));
    int d;
    for (; *txt >= '0' && *txt <= '9'; ++txt) { d = 1; }
    if (*txt == '.') {
      ++txt;
      for (; *txt >= '0' && *txt <= '9'; ++txt) { d = 1; }
      check(d);
    }
    if (*txt == 'e' || *txt == 'E') {
      ++txt;
      if (*txt == '-' || *txt == '+') {
        ++txt;
      }
      check(*txt >= '1' && *txt <= '9');
      for (; *txt >= '0' && *txt <= '9'; ++txt) { }
    }
    json->type = J_NUMBER;
    char *end;
    json->number = strtod(start, &end);
    check(end == txt);
    check(json->number != HUGE_VAL && json->number != -HUGE_VAL);
    break;
  }
  case '[': {
    struct JSON *array = NULL;
    int sz = 0;
    ++txt;
    for (;;) {
      for (; *txt > '\000' && *txt <= ' '; ++txt) { }
      if (*txt == ']') {
        ++txt;
        break;
      }
      check(array = realloc(array, sizeof(struct JSON)*++sz));
      txt = parseJSON(array + sz - 1, txt);
      for (; *txt > '\000' && *txt <= ' '; ++txt) { }
      check(*txt == ',' || *txt == ']');
      if (*txt == ',') {
        ++txt;
      }
    }
    json->type       = J_ARRAY;
    json->array      = array;
    json->array_size = sz;
    break;
  }
  case '{': {
    json->type = J_OBJECT;
    initHashMap(&json->obj, destroyJSONEntry, NULL);
    check(*++txt);
    for (;;) {
      for (; *txt > '\000' && *txt <= ' '; ++txt) { }
      if (*txt == '}') {
        ++txt;
        break;
      }
      check(*txt == '"');
      check(*++txt);
      char *key = parseString(&txt);
      check(*key);
      check(!getRefFromHashMap(&json->obj, key));
      for (; *txt > '\000' && *txt <= ' '; ++txt) { }
      check(*txt++ == ':');
      struct JSON *value;
      check(value = malloc(sizeof(struct JSON)));
      txt = parseJSON(value, txt);
      addToHashMap(&json->obj, key, (char *)value);
      for (; *txt > '\000' && *txt <= ' '; ++txt) { }
      check(*txt == ',' || *txt == '}');
      if (*txt == ',') {
        ++txt;
      }
    }
    break;
  }
  default:
    if (!memcmp(txt, "false", 5)) {
      json->type = J_FALSE;
      txt += 5;
    } else if (!memcmp(txt, "true", 4)) {
      json->type = J_TRUE;
      txt += 4;
    } else if (!memcmp(txt, "null", 4)) {
      json->type = J_NULL;
      txt += 4;
    } else {
      check(0);
    }
  }
  return txt;
}

struct JSON *newJSON(const char *txt) {
  struct JSON *json;
  check(json = malloc(sizeof(struct JSON)));
  initJSON(json, txt);
  return json;
}

void initJSON(struct JSON *json, const char *txt) {
  txt = parseJSON(json, txt);
  for (; *txt > '\000' && *txt <= ' '; ++txt) { }
  check(!*txt);
}

void destroyJSON(struct JSON *json) {
  if (json) {
    switch (json->type) {
    case J_STRING:
      free(json->str);
      break;
    case J_ARRAY:
      for (size_t i = 0; i < json->array_size; ++i) {
        destroyJSON(&json->array[i]);
      }
      free(json->array);
      break;
    case J_OBJECT:
      destroyHashMap(&json->obj);
      break;
    default:
      check(json->type == J_NUMBER ||
            json->type == J_FALSE  ||
            json->type == J_TRUE   ||
            json->type == J_NULL);
      break;
    }
  }
}

void deleteJSON(struct JSON *json) {
  destroyJSON(json);
  free(json);
}

void updateJSONValue(struct JSON *json, const char *key, const char *value) {
  switch (json->type) {
  case J_STRING:;
    // For strings, ignore the key and change the string to be the new value.
    size_t len = strlen(value);
    check(json->str = realloc(json->str, len + 1));
    strcpy(json->str, value);
    break;
  case J_NUMBER:;
    // Change the value of the number
    char *end;
    json->number = strtod(value, &end);
    check(!*end);
    check(json->number != HUGE_VAL && json->number != -HUGE_VAL);
    break;
  case J_ARRAY:
    // Append to the end of the array
    check(json->array = realloc(json->array,
                                ++json->array_size * sizeof(struct JSON)));
    initJSON(json->array + json->array_size - 1, value);
    break;
  case J_OBJECT:;
    // Add a new JSON entry to the object
    const char *k;
    check(k = strdup(key));
    addToHashMap(&json->obj, k, (char *)newJSON(value));
    break;
  case J_FALSE:
  case J_TRUE:
    // Change the truth value of the object
    if (!strcmp(value, "true")) {
      json->type = J_TRUE;
    } {
      check(!strcmp(value, "false"));
      json->type = J_FALSE;
    }
    break;
  case J_NULL:
    // Can't change a NULL value
    check(!strcmp(value, "null"));
    break;
  }
}

struct CmpObject {
  int rc;
  int count;
  const struct HashMap *obj;
  const struct HashMap *exclude;
};
static int cmpObject(void *arg, const char *key, char **value) {
  struct CmpObject *state = (struct CmpObject *)arg;
  if (state->rc &&
      (!state->exclude || !getFromHashMap(state->exclude, key))) {
    ++state->count;
    const struct JSON *a = (const struct JSON *)*value;
    const struct JSON *b = (const struct JSON *)getFromHashMap(state->obj,key);
    if (!isEqualJSON(a, b, state->exclude)) {
      state->rc = 0;
    }
  }
  return 1;
}

static int countExcluded(void *arg, const char *key, char **value) {
  struct CmpObject *state = (struct CmpObject *)arg;
  if (state->exclude && getFromHashMap(state->exclude, key)) {
    ++state->count;
  }
  return 1;
}

int isEqualJSON(const struct JSON *a, const struct JSON *b,
                const struct HashMap *exclude) {
  if (!a || !b) {
    return a == b;
  }
  if (a->type != b->type) {
    return 0;
  }
  switch (a->type) {
  case J_STRING:
    return !strcmp(a->str, b->str);
  case J_NUMBER:
    return a->number == b->number;
  case J_ARRAY:
    if (a->array_size != b->array_size) {
      return 0;
    }
    for (int i = 0; i < a->array_size; ++i) {
      if (!isEqualJSON(a->array + i, b->array + i, exclude)) {
        return 0;
      }
    }
    return 1;
  case J_OBJECT:
    if (!exclude && getHashmapSize(&a->obj) != getHashmapSize(&b->obj)) {
      return 0;
    }
    struct CmpObject state = {
      .rc      = 1,
      .count   = 0,
      .obj     = &b->obj,
      .exclude = exclude
    };
    iterateOverHashMap((struct HashMap *)&a->obj, cmpObject, &state);
    if (state.rc && exclude) {
      iterateOverHashMap((struct HashMap *)&b->obj, countExcluded, &state);
    }
    return state.rc && state.count == getHashmapSize(&b->obj);
  default:
    return 1;
  }
}
